/*
 * This file is part of Catfish.
 * Copyright (C) 2010 Namazu Studios LLC
 * 
 * Catfish is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of 
 * the License, or (at your option) any later version.
 * 
 * Catfish is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with Catfish.  If not, please visit:
 *  
 * http://www.gnu.org/licenses/
 *  
 */
package com.namazustudios.catfish.gae;

import static com.namazustudios.catfish.gae.GaeCatfish.entity;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.inject.Inject;
import com.namazustudios.catfish.CatfishException;
import com.namazustudios.catfish.Marshaller;
import com.namazustudios.catfish.annotation.Child;
import com.namazustudios.catfish.annotation.Flat;
import com.namazustudios.catfish.annotation.Property;
import com.namazustudios.catfish.converter.Converter;
import com.namazustudios.catfish.validator.ValidationFailure;
import com.namazustudios.catfish.validator.Validator;

public class GaeMarshaller implements Marshaller {

    @Inject
    private GaeCatfish catfish;

    @Inject(optional = true)
    private Map<String, Validator<?>> validators = Collections.emptyMap();

    @Inject(optional = true)
    private Map<String, Converter<?, ?>> converters = Collections.emptyMap();

    private final Map<Object, Entity> identities = new IdentityHashMap<Object, Entity>();

    public IdentityHashMap<Object, Entity> marshall(Object object) {
        Key key = catfish.getKey(object);
        Key parent = catfish.getKey(object);

        if (key != null) {
            parent = parent.getParent();
        }

        key = catfish.generateKey(object, parent);
        Entity entity = new Entity(key);

        try {
            identities.clear();
            doMarshall(entity, object);
            return new IdentityHashMap<Object, Entity>(identities);
        } finally {
            identities.clear();
        }
    }

    private void doMarshall(Entity entity, Object object) {
        final Key key = entity.getKey();
        Class<?> cls = object.getClass();

        // Checks if the object was already marshalled. If the object was
        // already marshalled, it just sets the new entitiy'sproperties if there
        // is a different entity.
        if (identities.containsKey(object)) {
            Entity other = identities.get(object);

            // Ensures that the object's key matches the cached identity, if
            // they dont' match then there is a set of conflicting keys and
            // that's no good.
            if (!entity.getKey().equals(catfish.getKey(object))) {
                throw new CatfishException("Conflicting keys.  Found " + "entity with key " + entity.getKey()
                        + " and object " + "with key " + catfish.getKey(object));
            }

            // Assigns the properies from the old entity to the new entity.
            if (other != entity) {
                entity.setPropertiesFrom(other);
            }

            // Since this object was already marshalled, simply return to
            // prevent infinite recursion.
            return;
        }

        do {
            for (Field field : cls.getDeclaredFields()) {
                boolean accessable = field.isAccessible();

                if (Modifier.isTransient(field.getModifiers())) {
                    continue;
                }

                try {

                    field.setAccessible(true);

                    if (field.isAnnotationPresent(Property.class)) {
                        marshallProperty(entity, object, field);
                    } else if (field.isAnnotationPresent(Child.class)) {
                        marshallChild(entity, object, field);
                    } else if (field.isAnnotationPresent(Flat.class)) {
                        Object child = field.get(object);

                        if (child != null) {
                            doMarshall(entity, child);                        	
                        }

                    }

                } catch (IllegalArgumentException ex) {
                    throw new CatfishException("Encountered error " + "while marshalling " + object + " with key "
                            + key + ".", ex);
                } catch (IllegalAccessException ex) {
                    throw new CatfishException("Encountered error " + "while marshalling " + object + " with key "
                            + key + ".", ex);
                } finally {
                    field.setAccessible(accessable);
                }

            }
        } while ((cls = cls.getSuperclass()) != null);

        if (!entity.getKey().equals(entity.getKey())) {
            throw new CatfishException("Conflicting keys.  Found " + "entity with key " + entity.getKey()
                    + " and object " + "with key " + catfish.getKey(object));
        }

        if (object.getClass().isAnnotationPresent(entity())) {
            identities.put(object, entity);
        }

    }

    @SuppressWarnings("unchecked")
    private void marshallProperty(Entity entity, Object object, Field field) throws ValidationFailure,
            IllegalAccessException {
        String propertyName;
        Object propertyValue;

        propertyName = field.getAnnotation(Property.class).name();
        propertyName = propertyName.trim();

        if (propertyName.isEmpty()) {
            propertyName = field.getName();
        }

        if (Collection.class.isAssignableFrom(field.getType())) {
            String name;
            ListIterator<Object> itr;
            Validator<Object> validator;
            Converter<Object, Object> converter;
            List<Object> tmp = new ArrayList<Object>((Collection<?>) field.get(object));

            name = field.getAnnotation(Property.class).validator();
            if (!name.isEmpty() && !validators.containsKey(name)) {
                throw new CatfishException("Validator \"" + name + "\" not found.");
            } else {
                validator = (Validator<Object>) validators.get(name);
            }

            if (validator != null) {
                itr = tmp.listIterator();
                try {
                    while (itr.hasNext()) {
                        Object element = itr.next();
                        element = validator.validate(element);
                        itr.set(element);
                    }
                } catch (ClassCastException ex) {
                    throw new CatfishException("Type mismatch while validating field " + field, ex);
                }
            }

            name = field.getAnnotation(Property.class).converter();
            if (!name.isEmpty() && !converters.containsKey(name)) {
                throw new CatfishException("Converter \"" + name + "\" not found.");
            } else {
                converter = (Converter<Object, Object>) converters.get(name);
            }

            if (converter != null) {
                itr = tmp.listIterator();
                try {
                    while (itr.hasNext()) {
                        Object element = itr.next();
                        element = converter.store(element);
                        itr.set(element);
                    }
                } catch (ClassCastException ex) {
                    throw new CatfishException("Type mismatch while converting field " + field, ex);
                }
            }

            entity.setProperty(propertyName, tmp);

        } else {
            propertyValue = validate(field, object);
            propertyValue = convert(field, object);
            entity.setProperty(propertyName, propertyValue);
        }

    }

    private void marshallChild(Entity entity, Object object, Field field) throws IllegalAccessException {
        Object child;
        String propertyName;

        propertyName = field.getAnnotation(Child.class).name().trim();

        if (propertyName.isEmpty()) {
            propertyName = field.getName();
        }

        child = field.get(object);

        if (child == null) {
            entity.removeProperty(propertyName);
        } else if (child instanceof Collection<?>) {
            List<Object> tmp = new ArrayList<Object>((Collection<?>) child);
            ListIterator<Object> itr = tmp.listIterator();

            while (itr.hasNext()) {
                Object element = itr.next();
                Key key = catfish.generateKey(element, catfish.getKey(object));
                Entity childEntity = new Entity(key);

                itr.set(key);
                doMarshall(childEntity, element);
            }

            entity.setProperty(propertyName, tmp);

        } else {
            Key key = catfish.generateKey(child, catfish.getKey(object));
            Entity childEntity = new Entity(key);
            entity.setProperty(propertyName, key);
            doMarshall(childEntity, child);
        }
    }

    @SuppressWarnings("unchecked")
    private Object validate(Field field, Object object, Object value) throws ValidationFailure, IllegalAccessException {
        Validator<Object> validator;
        String name = field.getAnnotation(Property.class).validator().trim();

        try {

            if (name.isEmpty()) {
                return value;
            } else if ((validator = (Validator<Object>) validators.get(name)) == null) {
                throw new CatfishException("Validator \"" + name + "\" not found.");
            }

            return validator.validate(value);
        } catch (ClassCastException ex) {
            throw new CatfishException("Type mismatch for validator.", ex);
        }
    }

    private Object validate(Field field, Object object) throws ValidationFailure, IllegalAccessException {
        return validate(field, object, field.get(object));
    }

    @SuppressWarnings("unchecked")
    private Object convert(Field field, Object object) throws IllegalAccessException {
        Converter<Object, Object> converter;
        String name = field.getAnnotation(Property.class).converter();

        try {

            if (name.isEmpty()) {
                return field.get(object);
            } else if ((converter = (Converter<Object, Object>) converters.get(name)) == null) {
                throw new CatfishException("Converter \"" + name + "\" not found.");
            }

            return converter.store(field.get(object));
        } catch (ClassCastException ex) {
            throw new CatfishException("Type mismatch for converter.", ex);
        }
    }
}
